# frozen_string_literal: true

module DiscoursePoll
  class PollsUpdater
    POLL_ATTRIBUTES = %w[close_at max min results status step type visibility title groups]

    def self.update(post, polls)
      ::Poll.transaction do
        has_changed = false
        edit_window = SiteSetting.poll_edit_window_mins

        old_poll_names = ::Poll.where(post: post).pluck(:name)
        new_poll_names = polls.keys

        deleted_poll_names = old_poll_names - new_poll_names
        created_poll_names = new_poll_names - old_poll_names

        # delete polls
        if deleted_poll_names.present?
          ::Poll.where(post: post, name: deleted_poll_names).destroy_all
        end

        # create polls
        if created_poll_names.present?
          has_changed = true
          polls.slice(*created_poll_names).values.each { |poll| Poll.create!(post.id, poll) }
        end

        # update polls
        ::Poll
          .includes(:poll_votes, :poll_options)
          .where(post: post)
          .find_each do |old_poll|
            new_poll = polls[old_poll.name]
            new_poll_options = new_poll["options"]

            attributes = new_poll.slice(*POLL_ATTRIBUTES)
            attributes["visibility"] = new_poll["public"] == "true" ? "everyone" : "secret"
            attributes["close_at"] = begin
              Time.zone.parse(new_poll["close"])
            rescue StandardError
              nil
            end
            attributes["status"] = old_poll["status"]
            attributes["groups"] = new_poll["groups"]
            poll = ::Poll.new(attributes)

            if is_different?(old_poll, poll, new_poll_options)
              # only prevent changes when there's at least 1 vote
              if old_poll.poll_votes.size > 0
                # can't change after edit window (when enabled)
                if edit_window > 0 && old_poll.created_at < edit_window.minutes.ago
                  error =
                    (
                      if poll.name == DiscoursePoll::DEFAULT_POLL_NAME
                        I18n.t(
                          "poll.edit_window_expired.cannot_edit_default_poll_with_votes",
                          minutes: edit_window,
                        )
                      else
                        I18n.t(
                          "poll.edit_window_expired.cannot_edit_named_poll_with_votes",
                          minutes: edit_window,
                          name: poll.name,
                        )
                      end
                    )

                  post.errors.add(:base, error)
                  # rubocop:disable Lint/NonLocalExitFromIterator
                  return
                  # rubocop:enable Lint/NonLocalExitFromIterator
                end
              end

              # update poll
              POLL_ATTRIBUTES.each do |attr|
                old_poll.public_send("#{attr}=", poll.public_send(attr))
              end

              old_poll.save!

              # keep track of anonymous votes
              anonymous_votes =
                old_poll.poll_options.map { |pv| [pv.digest, pv.anonymous_votes] }.to_h

              # destroy existing options & votes
              ::PollOption.where(poll: old_poll).destroy_all

              # create new options
              new_poll_options.each do |option|
                ::PollOption.create!(
                  poll: old_poll,
                  digest: option["id"],
                  html: option["html"].strip,
                  anonymous_votes: anonymous_votes[option["id"]],
                )
              end

              has_changed = true
            end
          end

        if ::Poll.exists?(post: post)
          post.custom_fields[HAS_POLLS] = true
        else
          post.custom_fields.delete(HAS_POLLS)
        end

        post.save_custom_fields(true)

        if has_changed
          polls = ::Poll.includes(poll_options: :poll_votes).where(post: post)
          polls =
            ActiveModel::ArraySerializer.new(
              polls,
              each_serializer: PollSerializer,
              root: false,
              scope: Guardian.new(nil),
            ).as_json
          post.publish_message!("/polls/#{post.topic_id}", post_id: post.id, polls: polls)
        end
      end
    end

    private

    def self.is_different?(old_poll, new_poll, new_options)
      # an attribute was changed?
      POLL_ATTRIBUTES.each do |attr|
        return true if old_poll.public_send(attr) != new_poll.public_send(attr)
      end

      sorted_old_options = old_poll.poll_options.map { |o| o.digest }.sort
      sorted_new_options = new_options.map { |o| o["id"] }.sort

      sorted_old_options != sorted_new_options
    end
  end
end
